/*
  ==============================================================================

   This file is part of the JUCE framework.
   Copyright (c) Raw Material Software Limited

   JUCE is an open source framework subject to commercial or open source
   licensing.

   By downloading, installing, or using the JUCE framework, or combining the
   JUCE framework with any other source code, object code, content or any other
   copyrightable work, you agree to the terms of the JUCE End User Licence
   Agreement, and all incorporated terms including the JUCE Privacy Policy and
   the JUCE Website Terms of Service, as applicable, which will bind you. If you
   do not agree to the terms of these agreements, we will not license the JUCE
   framework to you, and you must discontinue the installation or download
   process and cease use of the JUCE framework.

   JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
   JUCE Privacy Policy: https://juce.com/juce-privacy-policy
   JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/

   Or:

   You may also use this code under the terms of the AGPLv3:
   https://www.gnu.org/licenses/agpl-3.0.en.html

   THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
   WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
   MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.

  ==============================================================================
*/

#pragma once


//==============================================================================
class TabbedComponentHandler  : public ComponentTypeHandler
{
public:
    TabbedComponentHandler()
        : ComponentTypeHandler ("Tabbed Component", "juce::TabbedComponent", typeid (TabbedComponent), 200, 150)
    {}

    Component* createNewComponent (JucerDocument*) override
    {
        TabbedComponent* const t = new TabbedComponent (TabbedButtonBar::TabsAtTop);
        t->setName ("new tabbed component");

        for (int i = 3; --i >= 0;)
            addNewTab (t);

        return t;
    }

    XmlElement* createXmlFor (Component* comp, const ComponentLayout* layout) override
    {
        TabbedComponent* const t = dynamic_cast<TabbedComponent*> (comp);
        XmlElement* const e = ComponentTypeHandler::createXmlFor (comp, layout);

        if (t->getOrientation() == TabbedButtonBar::TabsAtTop)           e->setAttribute ("orientation", "top");
        else if (t->getOrientation() == TabbedButtonBar::TabsAtBottom)   e->setAttribute ("orientation", "bottom");
        else if (t->getOrientation() == TabbedButtonBar::TabsAtLeft)     e->setAttribute ("orientation", "left");
        else if (t->getOrientation() == TabbedButtonBar::TabsAtRight)    e->setAttribute ("orientation", "right");

        e->setAttribute ("tabBarDepth", t->getTabBarDepth());
        e->setAttribute ("initialTab", t->getCurrentTabIndex());

        for (int i = 0; i < t->getNumTabs(); ++i)
            e->addChildElement (getTabState (t, i).release());

        return e;
    }

    bool restoreFromXml (const XmlElement& xml, Component* comp, const ComponentLayout* layout) override
    {
        if (! ComponentTypeHandler::restoreFromXml (xml, comp, layout))
            return false;

        TabbedComponent* const t = dynamic_cast<TabbedComponent*> (comp);

        if (xml.getStringAttribute ("orientation") == "top")          t->setOrientation (TabbedButtonBar::TabsAtTop);
        else if (xml.getStringAttribute ("orientation") == "bottom")  t->setOrientation (TabbedButtonBar::TabsAtBottom);
        else if (xml.getStringAttribute ("orientation") == "left")    t->setOrientation (TabbedButtonBar::TabsAtLeft);
        else if (xml.getStringAttribute ("orientation") == "right")   t->setOrientation (TabbedButtonBar::TabsAtRight);

        TabbedComponent defaultTabComp (TabbedButtonBar::TabsAtTop);

        t->setTabBarDepth (xml.getIntAttribute ("tabBarDepth", defaultTabComp.getTabBarDepth()));

        t->clearTabs();

        for (auto* e : xml.getChildIterator())
        {
            addNewTab (t);
            restoreTabState (t, t->getNumTabs() - 1, *e);
        }

        t->setCurrentTabIndex (xml.getIntAttribute ("initialTab", 0));

        return true;
    }

    void getEditableProperties (Component* component, JucerDocument& doc,
                                Array<PropertyComponent*>& props, bool multipleSelected) override
    {
        ComponentTypeHandler::getEditableProperties (component, doc, props, multipleSelected);

        if (multipleSelected)
            return;

        if (auto* t = dynamic_cast<TabbedComponent*> (component))
        {
            props.add (new TabOrientationProperty (t, doc));
            props.add (new TabDepthProperty (t, doc));

            if (t->getNumTabs() > 0)
                props.add (new TabInitialTabProperty (t, doc));

            props.add (new TabAddTabProperty (t, doc));

            if (t->getNumTabs() > 0)
                props.add (new TabRemoveTabProperty (t, doc));
        }
    }

    void addPropertiesToPropertyPanel (Component* comp, JucerDocument& doc,
                                       PropertyPanel& panel, bool multipleSelected) override
    {
        ComponentTypeHandler::addPropertiesToPropertyPanel (comp, doc, panel, multipleSelected);

        TabbedComponent* const t = dynamic_cast<TabbedComponent*> (comp);

        for (int i = 0; i < t->getNumTabs(); ++i)
        {
            Array<PropertyComponent*> properties;

            properties.add (new TabNameProperty (t, doc, i));
            properties.add (new TabColourProperty (t, doc, i));

            properties.add (new TabContentTypeProperty (t, doc, i));

            if (isTabUsingJucerComp (t, i))
                properties.add (new TabJucerFileProperty (t, doc, i));
            else
                properties.add (new TabContentClassProperty (t, doc, i));

            properties.add (new TabContentConstructorParamsProperty (t, doc, i));

            properties.add (new TabMoveProperty (t, doc, i, t->getNumTabs()));

            panel.addSection ("Tab " + String (i), properties);
        }
    }

    String getCreationParameters (GeneratedCode&, Component* comp) override
    {
        TabbedComponent* const t = dynamic_cast<TabbedComponent*> (comp);

        switch (t->getOrientation())
        {
            case TabbedButtonBar::TabsAtTop:        return "juce::TabbedButtonBar::TabsAtTop";
            case TabbedButtonBar::TabsAtBottom:     return "juce::TabbedButtonBar::TabsAtBottom";
            case TabbedButtonBar::TabsAtLeft:       return "juce::TabbedButtonBar::TabsAtLeft";
            case TabbedButtonBar::TabsAtRight:      return "juce::TabbedButtonBar::TabsAtRight";
            default:                                jassertfalse; break;
        }

        return {};
    }

    void fillInCreationCode (GeneratedCode& code, Component* component, const String& memberVariableName) override
    {
        TabbedComponent* const t = dynamic_cast<TabbedComponent*> (component);

        ComponentTypeHandler::fillInCreationCode (code, component, memberVariableName);

        code.constructorCode
            << memberVariableName << "->setTabBarDepth (" << t->getTabBarDepth() << ");\n";

        for (int i = 0; i < t->getNumTabs(); ++i)
        {
            String contentClassName;

            if (isTabUsingJucerComp (t, i))
            {
                File jucerCpp = code.document->getCppFile().getSiblingFile (getTabJucerFile (t, i));

                std::unique_ptr<JucerDocument> doc (JucerDocument::createForCppFile (nullptr, jucerCpp));

                if (doc != nullptr)
                {
                    code.includeFilesCPP.add (jucerCpp.withFileExtension (".h"));
                    contentClassName = doc->getClassName();
                }
            }
            else
            {
                contentClassName = getTabClassName (t, i);
            }

            code.constructorCode
                << memberVariableName
                << "->addTab ("
                << quotedString (t->getTabNames() [i], code.shouldUseTransMacro())
                << ", "
                << CodeHelpers::colourToCode (t->getTabBackgroundColour (i));

            if (contentClassName.isNotEmpty())
            {
                code.constructorCode << ", new " << contentClassName;

                if (getTabConstructorParams (t, i).trim().isNotEmpty())
                    code.constructorCode << " ";

                code.constructorCode << "(" << getTabConstructorParams (t, i).trim() << "), true);\n";
            }
            else
            {
                code.constructorCode << ", 0, false);\n";
            }
        }

        code.constructorCode
            << memberVariableName << "->setCurrentTabIndex (" << t->getCurrentTabIndex() << ");\n";

        code.constructorCode << "\n";
    }

    //==============================================================================
    static void addNewTab (TabbedComponent* tc, const int insertIndex = -1)
    {
        tc->addTab ("Tab " + String (tc->getNumTabs()), Colours::lightgrey,
                    new TabDemoContentComp(), true, insertIndex);
    }

    //==============================================================================
    static std::unique_ptr<XmlElement> getTabState (TabbedComponent* tc, int tabIndex)
    {
        auto xml = std::make_unique<XmlElement> ("TAB");
        xml->setAttribute ("name", tc->getTabNames() [tabIndex]);
        xml->setAttribute ("colour", tc->getTabBackgroundColour (tabIndex).toString());

        if (TabDemoContentComp* const tdc = dynamic_cast<TabDemoContentComp*> (tc->getTabContentComponent (tabIndex)))
        {
            xml->setAttribute ("useJucerComp", tdc->isUsingJucerComp);
            xml->setAttribute ("contentClassName", tdc->contentClassName);
            xml->setAttribute ("constructorParams", tdc->constructorParams);
            xml->setAttribute ("jucerComponentFile", tdc->jucerComponentFile);
        }

        return xml;
    }

    static void restoreTabState (TabbedComponent* tc, int tabIndex, const XmlElement& xml)
    {
        tc->setTabName (tabIndex, xml.getStringAttribute ("name", "Tab"));
        tc->setTabBackgroundColour (tabIndex, Colour::fromString (xml.getStringAttribute ("colour", Colours::lightgrey.toString())));

        if (TabDemoContentComp* const tdc = dynamic_cast<TabDemoContentComp*> (tc->getTabContentComponent (tabIndex)))
        {
            tdc->isUsingJucerComp   = xml.getBoolAttribute ("useJucerComp", false);
            tdc->contentClassName   = xml.getStringAttribute ("contentClassName");
            tdc->constructorParams  = xml.getStringAttribute ("constructorParams");
            tdc->jucerComponentFile = xml.getStringAttribute ("jucerComponentFile");

            tdc->updateContent();
        }
    }

    //==============================================================================
    static bool isTabUsingJucerComp (TabbedComponent* tc, int tabIndex)
    {
        auto tdc = dynamic_cast<TabDemoContentComp*> (tc->getTabContentComponent (tabIndex));
        jassert (tdc != nullptr);

        return tdc != nullptr && tdc->isUsingJucerComp;
    }

    static void setTabUsingJucerComp (TabbedComponent* tc, int tabIndex, const bool b)
    {
        auto tdc = dynamic_cast<TabDemoContentComp*> (tc->getTabContentComponent (tabIndex));
        jassert (tdc != nullptr);

        if (tdc != nullptr)
        {
            tdc->isUsingJucerComp = b;
            tdc->updateContent();
        }
    }

    static String getTabClassName (TabbedComponent* tc, int tabIndex)
    {
        auto tdc = dynamic_cast<TabDemoContentComp*> (tc->getTabContentComponent (tabIndex));
        jassert (tdc != nullptr);

        return tdc != nullptr ? tdc->contentClassName : String();
    }

    static void setTabClassName (TabbedComponent* tc, int tabIndex, const String& newName)
    {
        TabDemoContentComp* const tdc = dynamic_cast<TabDemoContentComp*> (tc->getTabContentComponent (tabIndex));
        jassert (tdc != nullptr);

        if (tdc != nullptr)
        {
            tdc->contentClassName = newName;
            tdc->updateContent();
        }
    }

    static String getTabConstructorParams (TabbedComponent* tc, int tabIndex)
    {
        auto tdc = dynamic_cast<TabDemoContentComp*> (tc->getTabContentComponent (tabIndex));
        jassert (tdc != nullptr);

        return tdc != nullptr ? tdc->constructorParams : String();
    }

    static void setTabConstructorParams (TabbedComponent* tc, int tabIndex, const String& newParams)
    {
        auto tdc = dynamic_cast<TabDemoContentComp*> (tc->getTabContentComponent (tabIndex));
        jassert (tdc != nullptr);

        if (tdc != nullptr)
        {
            tdc->constructorParams = newParams;
            tdc->updateContent();
        }
    }

    static String getTabJucerFile (TabbedComponent* tc, int tabIndex)
    {
        auto tdc = dynamic_cast<TabDemoContentComp*> (tc->getTabContentComponent (tabIndex));
        jassert (tdc != nullptr);

        return tdc != nullptr ? tdc->jucerComponentFile : String();
    }

    static void setTabJucerFile (TabbedComponent* tc, int tabIndex, const String& newFile)
    {
        auto tdc = dynamic_cast<TabDemoContentComp*> (tc->getTabContentComponent (tabIndex));
        jassert (tdc != nullptr);

        if (tdc != nullptr)
        {
            tdc->jucerComponentFile = newFile;
            tdc->updateContent();
        }
    }

private:
    //==============================================================================
    class TabDemoContentComp : public Component
    {
    public:
        TabDemoContentComp()
            : isUsingJucerComp (false)
        {
            setSize (2048, 2048);
        }

        void paint (Graphics& g) override
        {
            if (jucerComp == nullptr)
                g.fillCheckerBoard (getLocalBounds().toFloat(), 50.0f, 50.0f,
                                    Colour::greyLevel (0.9f).withAlpha (0.4f),
                                    Colour::greyLevel (0.8f).withAlpha (0.4f));
        }

        void resized() override
        {
            if (jucerComp != nullptr)
            {
                jucerComp->setBounds (getLocalBounds());
                setOpaque (jucerComp->isOpaque());
            }
        }

        void updateContent()
        {
            if (isUsingJucerComp)
            {
                if (jucerComp == nullptr
                    || jucerComp->getOwnerDocument() == nullptr
                    || jucerComp->getFilename() != jucerComponentFile)
                {
                    jucerComp.reset();

                    jucerComp.reset (new TestComponent (ComponentTypeHandler::findParentDocument (this), nullptr, false));
                    jucerComp->setFilename (jucerComponentFile);
                    jucerComp->setToInitialSize();

                    addAndMakeVisible (jucerComp.get());
                }
            }
            else
            {
                jucerComp.reset();
            }

            resized();
        }

        void parentHierarchyChanged() override
        {
            updateContent();
        }

        bool isUsingJucerComp;
        String contentClassName, constructorParams;
        String jucerComponentFile;
        std::unique_ptr<TestComponent> jucerComp;
    };

    //==============================================================================
    class TabOrientationProperty  : public ComponentChoiceProperty<TabbedComponent>
    {
    public:
        TabOrientationProperty (TabbedComponent* comp, JucerDocument& doc)
            : ComponentChoiceProperty<TabbedComponent> ("tab position", comp, doc)
        {
            choices.add ("Tabs at top");
            choices.add ("Tabs at bottom");
            choices.add ("Tabs at left");
            choices.add ("Tabs at right");
        }

        void setIndex (int newIndex) override
        {
            const TabbedButtonBar::Orientation orientations[] = { TabbedButtonBar::TabsAtTop,
                                                                  TabbedButtonBar::TabsAtBottom,
                                                                  TabbedButtonBar::TabsAtLeft,
                                                                  TabbedButtonBar::TabsAtRight };

            document.perform (new TabOrienationChangeAction (component, *document.getComponentLayout(), orientations [newIndex]),
                              "Change TabComponent orientation");
        }

        int getIndex() const override
        {
            switch (component->getOrientation())
            {
                case TabbedButtonBar::TabsAtTop:    return 0;
                case TabbedButtonBar::TabsAtBottom: return 1;
                case TabbedButtonBar::TabsAtLeft:   return 2;
                case TabbedButtonBar::TabsAtRight:  return 3;
                default:                            jassertfalse; break;
            }

            return 0;
        }

    private:
        class TabOrienationChangeAction  : public ComponentUndoableAction<TabbedComponent>
        {
        public:
            TabOrienationChangeAction (TabbedComponent* const comp, ComponentLayout& l, const TabbedButtonBar::Orientation newState_)
                : ComponentUndoableAction<TabbedComponent> (comp, l),
                  newState (newState_)
            {
                oldState = comp->getOrientation();
            }

            bool perform() override
            {
                showCorrectTab();
                getComponent()->setOrientation (newState);
                changed();
                return true;
            }

            bool undo() override
            {
                showCorrectTab();
                getComponent()->setOrientation (oldState);
                changed();
                return true;
            }

            TabbedButtonBar::Orientation newState, oldState;
        };
    };

    //==============================================================================
    class TabInitialTabProperty  : public ComponentChoiceProperty<TabbedComponent>
    {
    public:
        TabInitialTabProperty (TabbedComponent* comp, JucerDocument& doc)
            : ComponentChoiceProperty<TabbedComponent> ("initial tab", comp, doc)
        {
            for (int i = 0; i < comp->getNumTabs(); ++i)
                choices.add ("Tab " + String (i) + ": \"" + comp->getTabNames() [i] + "\"");
        }

        void setIndex (int newIndex) override
        {
            document.perform (new InitialTabChangeAction (component, *document.getComponentLayout(), newIndex),
                              "Change initial tab");
        }

        int getIndex() const override
        {
            return component->getCurrentTabIndex();
        }

    private:
        class InitialTabChangeAction  : public ComponentUndoableAction<TabbedComponent>
        {
        public:
            InitialTabChangeAction (TabbedComponent* const comp, ComponentLayout& l, const int newValue_)
                : ComponentUndoableAction<TabbedComponent> (comp, l),
                  newValue (newValue_)
            {
                oldValue = comp->getCurrentTabIndex();
            }

            bool perform() override
            {
                showCorrectTab();
                getComponent()->setCurrentTabIndex (newValue);
                changed();
                return true;
            }

            bool undo() override
            {
                showCorrectTab();
                getComponent()->setCurrentTabIndex (oldValue);
                changed();
                return true;
            }

        private:
            int newValue, oldValue;
        };
    };

    //==============================================================================
    class TabDepthProperty  : public SliderPropertyComponent,
                              private ChangeListener
    {
    public:
        TabDepthProperty (TabbedComponent* comp, JucerDocument& doc)
            : SliderPropertyComponent ("tab depth", 10.0, 80.0, 1.0, 1.0),
              component (comp),
              document (doc)
        {
            document.addChangeListener (this);
        }

        ~TabDepthProperty() override
        {
            document.removeChangeListener (this);
        }

        void setValue (double newValue) override
        {
            document.getUndoManager().undoCurrentTransactionOnly();

            document.perform (new TabDepthChangeAction (component, *document.getComponentLayout(), roundToInt (newValue)),
                              "Change TabComponent tab depth");
        }

        double getValue() const override
        {
            return component->getTabBarDepth();
        }

        TabbedComponent* const component;
        JucerDocument& document;

    private:
        void changeListenerCallback (ChangeBroadcaster*) override
        {
            refresh();
        }

        class TabDepthChangeAction  : public ComponentUndoableAction<TabbedComponent>
        {
        public:
            TabDepthChangeAction (TabbedComponent* const comp, ComponentLayout& l, const int newState_)
                : ComponentUndoableAction<TabbedComponent> (comp, l),
                  newState (newState_)
            {
                oldState = comp->getTabBarDepth();
            }

            bool perform() override
            {
                showCorrectTab();
                getComponent()->setTabBarDepth (newState);
                changed();
                return true;
            }

            bool undo() override
            {
                showCorrectTab();
                getComponent()->setTabBarDepth (oldState);
                changed();
                return true;
            }

            int newState, oldState;
        };
    };

    //==============================================================================
    class TabAddTabProperty  : public ButtonPropertyComponent
    {
    public:
        TabAddTabProperty (TabbedComponent* comp, JucerDocument& doc)
            : ButtonPropertyComponent ("add tab", false),
              component (comp),
              document (doc)
        {
        }

        void buttonClicked() override
        {
            document.perform (new AddTabAction (component, *document.getComponentLayout()),
                              "Add a new tab");
        }

        String getButtonText() const override
        {
            return "Create a new tab";
        }

        TabbedComponent* const component;
        JucerDocument& document;

    private:
        class AddTabAction  : public ComponentUndoableAction<TabbedComponent>
        {
        public:
            AddTabAction (TabbedComponent* const comp, ComponentLayout& l)
                : ComponentUndoableAction<TabbedComponent> (comp, l)
            {
            }

            bool perform() override
            {
                showCorrectTab();
                addNewTab (getComponent());
                layout.getDocument()->refreshAllPropertyComps();
                changed();
                return true;
            }

            bool undo() override
            {
                showCorrectTab();
                getComponent()->removeTab (getComponent()->getNumTabs() - 1);
                layout.getDocument()->refreshAllPropertyComps();
                changed();
                return true;
            }
        };
    };

    //==============================================================================
    class TabRemoveTabProperty  : public ButtonPropertyComponent
    {
    public:
        TabRemoveTabProperty (TabbedComponent* comp, JucerDocument& doc)
            : ButtonPropertyComponent ("remove tab", true),
              component (comp),
              document (doc)
        {
        }

        void buttonClicked() override
        {
            const StringArray names (component->getTabNames());

            PopupMenu m;
            for (int i = 0; i < component->getNumTabs(); ++i)
                m.addItem (i + 1, "Delete tab " + String (i)
                                    + ": \"" + names[i] + "\"");

            PopupMenu::Options options{};
            m.showMenuAsync (PopupMenu::Options().withTargetComponent (this), [this] (int r)
            {
                if (r > 0)
                    document.perform (new RemoveTabAction (component, *document.getComponentLayout(), r - 1),
                                      "Remove a tab");
            });
        }

        String getButtonText() const override
        {
            return "Delete a tab...";
        }

        TabbedComponent* const component;
        JucerDocument& document;

    private:
        class RemoveTabAction  : public ComponentUndoableAction<TabbedComponent>
        {
        public:
            RemoveTabAction (TabbedComponent* const comp, ComponentLayout& l, int indexToRemove_)
                : ComponentUndoableAction<TabbedComponent> (comp, l),
                  indexToRemove (indexToRemove_)
            {
                previousState = getTabState (comp, indexToRemove);
            }

            bool perform() override
            {
                showCorrectTab();

                getComponent()->removeTab (indexToRemove);
                layout.getDocument()->refreshAllPropertyComps();
                changed();
                return true;
            }

            bool undo() override
            {
                showCorrectTab();
                addNewTab (getComponent(), indexToRemove);
                restoreTabState (getComponent(), indexToRemove, *previousState);
                layout.getDocument()->refreshAllPropertyComps();
                changed();
                return true;
            }

        private:
            int indexToRemove;
            std::unique_ptr<XmlElement> previousState;
        };
    };

    //==============================================================================
    class TabNameProperty  : public ComponentTextProperty<TabbedComponent>
    {
    public:
        TabNameProperty (TabbedComponent* comp, JucerDocument& doc, const int tabIndex_)
            : ComponentTextProperty<TabbedComponent> ("name", 200, false, comp, doc),
              tabIndex (tabIndex_)
        {
        }

        void setText (const String& newText) override
        {
            document.perform (new TabNameChangeAction (component, *document.getComponentLayout(), tabIndex, newText),
                              "Change tab name");
        }

        String getText() const override
        {
            return component->getTabNames() [tabIndex];
        }

    private:
        int tabIndex;

        class TabNameChangeAction  : public ComponentUndoableAction<TabbedComponent>
        {
        public:
            TabNameChangeAction (TabbedComponent* const comp, ComponentLayout& l, const int tabIndex_, const String& newValue_)
                : ComponentUndoableAction<TabbedComponent> (comp, l),
                  tabIndex (tabIndex_),
                  newValue (newValue_)
            {
                oldValue = comp->getTabNames() [tabIndex];
            }

            bool perform() override
            {
                showCorrectTab();
                getComponent()->setTabName (tabIndex, newValue);
                changed();
                return true;
            }

            bool undo() override
            {
                showCorrectTab();
                getComponent()->setTabName (tabIndex, oldValue);
                changed();
                return true;
            }

        private:
            const int tabIndex;
            String newValue, oldValue;
        };
    };

    //==============================================================================
    class TabColourProperty  : public JucerColourPropertyComponent,
                               private ChangeListener
    {
    public:
        TabColourProperty (TabbedComponent* comp, JucerDocument& doc, const int tabIndex_)
            : JucerColourPropertyComponent ("colour", false),
              component (comp),
              document (doc),
              tabIndex (tabIndex_)
        {
            document.addChangeListener (this);
        }

        ~TabColourProperty() override
        {
            document.removeChangeListener (this);
        }

        void setColour (Colour newColour) override
        {
            document.getUndoManager().undoCurrentTransactionOnly();

            document.perform (new TabColourChangeAction (component, *document.getComponentLayout(), tabIndex, newColour),
                              "Change tab colour");
        }

        Colour getColour() const override
        {
            return component->getTabBackgroundColour (tabIndex);
        }

        void resetToDefault() override
        {
            jassertfalse; // shouldn't get called
        }

        void changeListenerCallback (ChangeBroadcaster*) override     { refresh(); }

    private:
        TabbedComponent* component;
        JucerDocument& document;
        int tabIndex;

        class TabColourChangeAction  : public ComponentUndoableAction<TabbedComponent>
        {
        public:
            TabColourChangeAction (TabbedComponent* comp, ComponentLayout& l,
                                   int tabIndex_, Colour newValue_)
                : ComponentUndoableAction<TabbedComponent> (comp, l),
                  tabIndex (tabIndex_),
                  newValue (newValue_)
            {
                oldValue = comp->getTabBackgroundColour (tabIndex);
            }

            bool perform() override
            {
                showCorrectTab();
                getComponent()->setTabBackgroundColour (tabIndex, newValue);
                changed();
                return true;
            }

            bool undo() override
            {
                showCorrectTab();
                getComponent()->setTabBackgroundColour (tabIndex, oldValue);
                changed();
                return true;
            }

        private:
            const int tabIndex;
            Colour newValue, oldValue;
        };
    };

    //==============================================================================
    class TabContentTypeProperty  : public ComponentChoiceProperty<TabbedComponent>
    {
    public:
        TabContentTypeProperty (TabbedComponent* comp, JucerDocument& doc, const int tabIndex_)
            : ComponentChoiceProperty<TabbedComponent> ("content type", comp, doc),
              tabIndex (tabIndex_)
        {
            choices.add ("Jucer content component");
            choices.add ("Named content component");
        }

        void setIndex (int newIndex) override
        {
            document.perform (new TabContentTypeChangeAction (component, *document.getComponentLayout(), tabIndex, newIndex == 0),
                              "Change tab content type");
        }

        int getIndex() const override
        {
            return isTabUsingJucerComp (component, tabIndex) ? 0 : 1;
        }

    private:
        int tabIndex;

        class TabContentTypeChangeAction  : public ComponentUndoableAction<TabbedComponent>
        {
        public:
            TabContentTypeChangeAction (TabbedComponent* const comp, ComponentLayout& l, const int tabIndex_, const bool newValue_)
                : ComponentUndoableAction<TabbedComponent> (comp, l),
                  tabIndex (tabIndex_),
                  newValue (newValue_)
            {
                oldValue = isTabUsingJucerComp (comp, tabIndex);
            }

            bool perform() override
            {
                showCorrectTab();
                setTabUsingJucerComp (getComponent(), tabIndex, newValue);
                layout.getDocument()->refreshAllPropertyComps();
                changed();
                return true;
            }

            bool undo() override
            {
                showCorrectTab();
                setTabUsingJucerComp (getComponent(), tabIndex, oldValue);
                layout.getDocument()->refreshAllPropertyComps();
                changed();
                return true;
            }

        private:
            int tabIndex;
            bool newValue, oldValue;
        };
    };

    //==============================================================================
    class TabJucerFileProperty   : public FilePropertyComponent,
                                   private ChangeListener
    {
    public:
        TabJucerFileProperty (TabbedComponent* const comp, JucerDocument& doc, const int tabIndex_)
            : FilePropertyComponent ("jucer file", false, true),
              component (comp),
              document (doc),
              tabIndex (tabIndex_)
        {
            document.addChangeListener (this);
        }

        ~TabJucerFileProperty() override
        {
            document.removeChangeListener (this);
        }

        //==============================================================================
        void setFile (const File& newFile) override
        {
            document.perform (new JucerCompFileChangeAction (component, *document.getComponentLayout(), tabIndex,
                                                             newFile.getRelativePathFrom (document.getCppFile().getParentDirectory())
                                                                    .replaceCharacter ('\\', '/')),
                              "Change tab component file");
        }

        File getFile() const override
        {
            return document.getCppFile().getSiblingFile (getTabJucerFile (component, tabIndex));
        }

    private:
        void changeListenerCallback (ChangeBroadcaster*) override { refresh(); }

        TabbedComponent* const component;
        JucerDocument& document;
        int tabIndex;

        class JucerCompFileChangeAction  : public ComponentUndoableAction<TabbedComponent>
        {
        public:
            JucerCompFileChangeAction (TabbedComponent* const comp, ComponentLayout& l, const int tabIndex_, const String& newState_)
                : ComponentUndoableAction<TabbedComponent> (comp, l),
                  tabIndex (tabIndex_),
                  newState (newState_)
            {
                oldState = getTabJucerFile (comp, tabIndex);
            }

            bool perform() override
            {
                showCorrectTab();
                setTabJucerFile (getComponent(), tabIndex, newState);
                changed();
                return true;
            }

            bool undo() override
            {
                showCorrectTab();
                setTabJucerFile (getComponent(), tabIndex, oldState);
                changed();
                return true;
            }

            int tabIndex;
            String newState, oldState;
        };
    };

    //==============================================================================
    class TabContentClassProperty   : public ComponentTextProperty<TabbedComponent>
    {
    public:
        TabContentClassProperty (TabbedComponent* comp, JucerDocument& doc, const int tabIndex_)
            : ComponentTextProperty<TabbedComponent> ("content class", 256, false, comp, doc),
              tabIndex (tabIndex_)
        {
        }

        void setText (const String& newText) override
        {
            document.perform (new TabClassNameChangeAction (component, *document.getComponentLayout(), tabIndex, newText),
                              "Change TabbedComponent content class");
        }

        String getText() const override
        {
            return getTabClassName (component, tabIndex);
        }

    private:
        int tabIndex;

        class TabClassNameChangeAction  : public ComponentUndoableAction<TabbedComponent>
        {
        public:
            TabClassNameChangeAction (TabbedComponent* const comp, ComponentLayout& l, const int tabIndex_, const String& newValue_)
                : ComponentUndoableAction<TabbedComponent> (comp, l),
                  tabIndex (tabIndex_),
                  newValue (newValue_)
            {
                oldValue = getTabClassName (comp, tabIndex);
            }

            bool perform() override
            {
                showCorrectTab();
                setTabClassName (getComponent(), tabIndex, newValue);
                changed();
                layout.getDocument()->refreshAllPropertyComps();
                return true;
            }

            bool undo() override
            {
                showCorrectTab();
                setTabClassName (getComponent(), tabIndex, oldValue);
                changed();
                layout.getDocument()->refreshAllPropertyComps();
                return true;
            }

            int tabIndex;
            String newValue, oldValue;
        };
    };

    //==============================================================================
    class TabContentConstructorParamsProperty   : public ComponentTextProperty<TabbedComponent>
    {
    public:
        TabContentConstructorParamsProperty (TabbedComponent* comp, JucerDocument& doc, const int tabIndex_)
            : ComponentTextProperty<TabbedComponent> ("constructor params", 512, false, comp, doc),
              tabIndex (tabIndex_)
        {
        }

        void setText (const String& newText) override
        {
            document.perform (new TabConstructorParamChangeAction (component, *document.getComponentLayout(), tabIndex, newText),
                              "Change TabbedComponent content constructor param");
        }

        String getText() const override
        {
            return getTabConstructorParams (component, tabIndex);
        }

    private:
        int tabIndex;

        class TabConstructorParamChangeAction  : public ComponentUndoableAction<TabbedComponent>
        {
        public:
            TabConstructorParamChangeAction (TabbedComponent* const comp, ComponentLayout& l, const int tabIndex_, const String& newValue_)
                : ComponentUndoableAction<TabbedComponent> (comp, l),
                  tabIndex (tabIndex_),
                  newValue (newValue_)
            {
                oldValue = getTabConstructorParams (comp, tabIndex);
            }

            bool perform() override
            {
                showCorrectTab();
                setTabConstructorParams (getComponent(), tabIndex, newValue);
                changed();
                layout.getDocument()->refreshAllPropertyComps();
                return true;
            }

            bool undo() override
            {
                showCorrectTab();
                setTabConstructorParams (getComponent(), tabIndex, oldValue);
                changed();
                layout.getDocument()->refreshAllPropertyComps();
                return true;
            }

            int tabIndex;
            String newValue, oldValue;
        };
    };

    //==============================================================================
    class TabMoveProperty   : public ButtonPropertyComponent
    {
    public:
        TabMoveProperty (TabbedComponent* comp, JucerDocument& doc,
                         const int tabIndex_, const int totalNumTabs_)
            : ButtonPropertyComponent ("move tab", false),
              component (comp),
              document (doc),
              tabIndex (tabIndex_),
              totalNumTabs (totalNumTabs_)
        {
        }

        void buttonClicked() override
        {
            PopupMenu m;
            m.addItem (1, "Move this tab up", tabIndex > 0);
            m.addItem (2, "Move this tab down", tabIndex < totalNumTabs - 1);

            PopupMenu::Options options{};
            m.showMenuAsync (PopupMenu::Options().withTargetComponent (this), [this] (int r)
            {
                if (r != 0)
                    document.perform (new MoveTabAction (component, *document.getComponentLayout(), tabIndex, tabIndex + (r == 2 ? 1 : -1)),
                                      "Move a tab");
            });
        }

        String getButtonText() const override
        {
            return "Move this tab...";
        }

        TabbedComponent* const component;
        JucerDocument& document;
        const int tabIndex, totalNumTabs;

    private:
        class MoveTabAction  : public ComponentUndoableAction<TabbedComponent>
        {
        public:
            MoveTabAction (TabbedComponent* const comp, ComponentLayout& l,
                           const int oldIndex_, const int newIndex_)
                : ComponentUndoableAction<TabbedComponent> (comp, l),
                  oldIndex (oldIndex_),
                  newIndex (newIndex_)
            {
            }

            void move (int from, int to)
            {
                showCorrectTab();

                auto state = getTabState (getComponent(), from);

                getComponent()->removeTab (from);
                addNewTab (getComponent(), to);

                restoreTabState (getComponent(), to, *state);

                layout.getDocument()->refreshAllPropertyComps();
                changed();
            }

            bool perform() override
            {
                move (oldIndex, newIndex);
                return true;
            }

            bool undo() override
            {
                move (newIndex, oldIndex);
                return true;
            }

        private:
            const int oldIndex, newIndex;
        };
    };
};
